﻿
using System.Text;

namespace SqlFormat {
    public abstract class SqlFormatter<OPTIONS_TYPE> where OPTIONS_TYPE : SqlFormatterOptions {
        private readonly OPTIONS_TYPE _options = null;
        private int _indent;

        public SqlFormatter(OPTIONS_TYPE options) {
            _options = options;
        }

        public virtual string Format(string sql) {
            _indent = 0;
            StringBuilder results = new StringBuilder();
            bool lastCharWasDelimiter;
            bool thisCharIsDelimiter = true;
            string keyword;
            for (int i = 0; i < sql.Length; ) {
                lastCharWasDelimiter = thisCharIsDelimiter;
                thisCharIsDelimiter = IsDelimiter(sql[i]);
                if (sql[i] == '\"') {
                    results.Append(BeforeString);
                    results.Append(sql[i]);
                    i++;
                    for (; i < sql.Length; i++) {
                        results.Append(sql[i]);
                        if (sql[i] == '\"') {
                            i++;
                            break;
                        }
                    }
                    results.Append(AfterString);
                } else if (sql[i] == '\'') {
                    results.Append(BeforeString);
                    results.Append(sql[i]);
                    i++;
                    for (; i < sql.Length; i++) {
                        results.Append(sql[i]);
                        if (sql[i] == '\'') {
                            i++;
                            break;
                        }
                    }
                    results.Append(AfterString);
                } else if (char.IsWhiteSpace(sql[i])) {
                    do {
                        i++;
                    } while (char.IsWhiteSpace(sql[i]));
                    results.Append(' ');
                } else if (lastCharWasDelimiter && IsKeyWord(sql.Substring(i), out keyword)) {
                    bool isIndentingKeyword = IsIndentingKeyword(keyword);
                    if (Options.Multiline && i > 0) {
                        if (isIndentingKeyword) {
                            DecrementIndent();
                            TrimTrailingWhiteSpace(results);
                            results.Append(NewLineString);
                        } else if (IsLineBreakBeforeKeyword(keyword)) {
                            TrimTrailingWhiteSpace(results);
                            results.Append(NewLineString);
                        }
                    }
                    results.Append(BeforeKeyWordString);
                    results.Append(keyword);
                    results.Append(AfterKeyWordString);
                    i += keyword.Length;
                    if (isIndentingKeyword && Options.Multiline) {
                        IncrementIndent();
                        results.Append(NewLineString);
                        while (char.IsWhiteSpace(sql[i])) {
                            i++;
                        }
                    }
                } else {
                    results.Append(sql[i]);
                    i++;
                }
            }
            return results.ToString();
        }

        private void TrimTrailingWhiteSpace(StringBuilder results) {
            int i = results.Length - 1;
            while (char.IsWhiteSpace(results[i])) {
                results.Remove(i, 1);
                i--;
            }
        }

        private void IncrementIndent() {
            _indent++;
        }

        private void DecrementIndent() {
            _indent--;
            if (_indent < 0) {
                _indent = 0;
            }
        }

        protected virtual bool IsIndentingKeyword(string keyword) {
            foreach (string ik in INDENTING_KEYWORDS) {
                if (ik == keyword) {
                    return true;
                }
            }
            return false;
        }

        protected virtual bool IsLineBreakBeforeKeyword(string keyword) {
            foreach (string lbbk in LINE_BREAK_BEFORE_KEYWORDS) {
                if (lbbk == keyword) {
                    return true;
                }
            }
            return false;
        }

        protected virtual bool IsKeyWord(string sqlStringPart, out string matchedKeyword) {
            sqlStringPart = sqlStringPart.ToUpperInvariant();
            foreach (string keyword in KEYWORDS) {
                if (sqlStringPart.StartsWith(keyword)) {
                    if (keyword.Length >= sqlStringPart.Length || IsDelimiter(sqlStringPart[keyword.Length])) {
                        matchedKeyword = keyword;
                        return true;
                    }
                }
            }
            matchedKeyword = null;
            return false;
        }

        /// <summary>
        /// @%^&*()-+|\/[];"', .
        /// </summary>
        protected virtual bool IsDelimiter(char c) {
            if (char.IsWhiteSpace(c)) return true;
            if (c == '@') return true;
            if (c == '%') return true;
            if (c == '^') return true;
            if (c == '&') return true;
            if (c == '*') return true;
            if (c == '(') return true;
            if (c == ')') return true;
            if (c == '-') return true;
            if (c == '+') return true;
            if (c == '|') return true;
            if (c == '\\') return true;
            if (c == '/') return true;
            if (c == '[') return true;
            if (c == ']') return true;
            if (c == ';') return true;
            if (c == '"') return true;
            if (c == '\'') return true;
            if (c == ',') return true;
            if (c == '.') return true;
            return false;
        }

        protected int Indent {
            get { return _indent; }
        }

        protected abstract string AfterString { get; }

        protected abstract string BeforeString { get; }

        protected abstract string BeforeKeyWordString { get; }

        protected abstract string AfterKeyWordString { get; }

        protected abstract string NewLineString { get; }

        public OPTIONS_TYPE Options {
            get { return _options; }
        }

        private static readonly string[] KEYWORDS = {
            "DELETE FROM",
            "INSERT INTO",

            "ADD", "ALL", "ALTER", "AND", "ANY", "AS", "ASC", "AUTHORIZATION", "AVG",
            "BACKUP", "BEGIN", "BETWEEN", "BREAK", "BROWSE", "BULK", "BY",
            "CASCADE", "CASE", "CHECK", "CHECKPOINT", "CLOSE", "CLUSTERED", "COALESCE", "COLUMN", "COMMIT", 
            "COMMITTED", "COMPUTE", "CONFIRM", "CONSTRAINT", "CONTAINS", "CONTAINSTABLE", "CONTINUE", "CONTROLROW",
            "CONVERT", "COUNT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP",
            "CURRENT_USER", "CURSOR",
            "DATABASE", "DBCC", "DEALLOCATE", "DECLARE", "DEFAULT", "DELETE", "DENY", "DESC", "DISK", "DISTINCT",
            "DISTRIBUTED", "DOUBLE", "DROP", "DUMMY", "DUMP",
            "ELSE", "END", "ERRLVL", "ERROREXIT", "ESCAPE", "EXCEPT", "EXEC", "EXECUTE", "EXISTS", "EXIT",
            "FETCH", "FILE", "FILLFACTOR", "FLOPPY", "FOR", "FOREIGN", "FREETEXT", "FREETEXTTABLE", "FROM", "FULL",
            "GO", "GOTO", "GRANT", "GROUP",
            "HAVING", "HOLDLOCK",
            "IDENTITY", "IDENTITY_INSERT", "IDENTITYCOL", "IF", "IN", "INDEX", "INNER", "INSERT", "INTERSECT", "INTO",
            "IS", "ISOLATION",
            "JOIN",
            "KEY", "KILL",
            "LEFT", "LEVEL", "LIKE", "LINENO", "LOAD",
            "MAX", "MIN", "MIRROREXIT",
            "NATIONAL", "NOCHECK", "NONCLUSTERED", "NOT", "NULL", "NULLIF",
            "OF", "OFF", "OFFSETS", "ON", "ONCE", "ONLY", "OPEN", "OPENDATASOURCE", "OPENQUERY", "OPENROWSET", "OPTION",
            "OR", "ORDER", "OUTER", "OVER",
            "PERCENT", "PERM", "PERMANENT", "PIPE", "PLAN", "PRECISION", "PREPARE", "PRIMARY", "PRINT", "PRIVILEGES",
            "PROC", "PROCEDURE", "PROCESSEXIT", "PUBLIC",
            "RAISERROR", "READ", "READTEXT", "RECONFIGURE", "REFERENCES", "REPEATABLE", "REPLICATION", "RESTORE",
            "RESTRICT", "RETURN", "REVOKE", "RIGHT", "ROLLBACK", "ROWCOUNT", "ROWGUIDCOL", "RULE",
            "SAVE", "SCHEMA", "SELECT", "SERIALIZABLE", "SESSION_USER", "SET", "SETUSER", "SHUTDOWN", "SOME",
            "STATISTICS", "SUM", "SYSTEM_USER",
            "TABLE", "TAPE", "TEMP", "TEMPORARY", "TEXTSIZE", "THEN", "TO", "TOP", "TRAN", "TRANSACTION", "TRIGGER",
            "TRUNCATE", "TSEQUAL", "UNCOMMITTED", "UNION", "UNIQUE", "UPDATE", "UPDATETEXT",
            "USE", "USER",
            "VALUES", "VARYING", "VIEW",
            "WAITFOR", "WHEN", "WHERE", "WHILE", "WITH", "WORK", "WRITETEXT"
        };

        private static readonly string[] INDENTING_KEYWORDS = 
        {
            "DELETE FROM",
            "FROM",
            "INSERT INTO",
            "SELECT", "SET",
            "UPDATE",
            "VALUES",
            "WHERE"
        };

        private static readonly string[] LINE_BREAK_BEFORE_KEYWORDS = 
        {
            "AND"                                                               
        };
    }
}
